0%

PEP475-Retry system calls failing with EINTR

PEP: 475
Title: Retry system calls failing with EINTR
Version: $Revision$
Last-Modified: $Date$
Author: Charles-François Natali cf.natali@gmail.com, Victor Stinner victor.stinner@gmail.com
BDFL-Delegate: Antoine Pitrou solipsis@pitrou.net
Status: Final
Type: Standards Track
Content-Type: text/x-rst
Created: 29-July-2014
Python-Version: 3.5
Resolution: https://mail.python.org/pipermail/python-dev/2015-February/138018.html

摘要

标准库中提供的系统调用函数在捕获到 EINTR 错误时能够自动重试,以减轻程序编码的负担。

所谓系统调用,我们指的是标准C函数库提供的操作 I/O 或者其他系统资源的函数。

基本原理

中断系统调用

在 POSIX 系统中,信号是很常见的,系统调用编码的时候必须准备捕获它们。一些常见的例子:

  • 最常见的是 SIGINT 信号,当按下 CTRL+C 时发送该信号。 Python 在默认情况下捕获该信号后会抛出一个 KeyboardInterrupt 异常。

  • 当使用到子进程时,子进程退出时会发送 SIGCHLD 信号。

  • 改变终端窗口大小时会向在该终端中运行的应用程序发送 SIGWINCH 信号。

  • 通过 CTRL+z 或者 SIGWINCH 命令将应用程序放到后台执行时发送 SIGCONT 信号。

编写一个安全的 C 信号处理器是困难的:因为并不是所有的函数都是 “异步信号安全的” (例如,printf()malloc() 函数就不是异步信号安全的),同时要处理好信号中断后的重入也是很麻烦的。然后幸运的是,当进程在执行系统调用的过程中被信号中断而失败的时会返回 EINTR 错误以便于程序处理,这样就不必强制要求函数是信号安全的。

这是一种依赖于系统的行为:在某些系统中,设置了 SA_RESTART 标识后,一些系统调用在捕获到 EINTR 错误后会自动重试。尽管如此,当在 Python 中调用 signal.signal() 函数设置信号处理器后会清除 SA_RESTART 标识:这样一来,在 Python 中所有的系统调用都可能会被信号中断而导致失败。

因为接收到一个信号并不是发生异常,所以健壮的 POSIX 编程要求必须能够处理 EINTR 错误(在大多情况下,也就意味着将一个希望操作成功的系统调用函数包装在一个循环中)。如果没有 python 提供的原生支持,应用程序编码就会更繁琐(由于python提供了支持,所以我们不需要专门去处理 EINTR 错误,这样可以使 python 代码更简洁。)。

Python 3.4 中的情况

在 Python 3.4 中,捕获 InterruptedError 异常(专门用于包装 EINTR 错误的异常类型)的代码被复制到各处系统调用处。但实际上也只有一小部分模块捕获了该异常,要修复这个问题让所有的 Python 模块都处理该异常需要花费好几年的时间。下面是一段捕获 InterruptedError 异常并自动重试 file.read() 的代码示例:

1
2
3
4
5
6
while True:
try:
data = file.read(size)
break
except InterruptedError:
continue

Python 标准库中已经实现内部捕获 InterruptedError 异常的模块列表:

  • asyncio
  • asyncore
  • io, _pyio
  • multiprocessing
  • selectors
  • socket
  • socketserver
  • subprocess

其他编程语言比如 Perl, Java 和 Go 中系统调用时捕获 EINTR 错误并自动重试已经在语言底层实现了,所以不会影响到库和应用程序。

Use Case 1: Don’t Bother With Signals

在大多数场景中,你可能并不想被信号中断,也不希望捕获到 InterruptedError 异常。举个例子,你真的希望为了一个 “Hello World” 的示例代码而写上一段如此复杂的代码吗?

1
2
3
4
5
6
while True:
try:
print("Hello World")
break
except InterruptedError:
continue

InterruptedError 异常可能发生在任何一个你不期望发生的地方。举个例子,os.close()FileIO.close() 调用都可能抛出 InterruptedError 。参见这篇文章:close() and EINTR

下面有关 Python issues related to EINTR 的章节将会给出一些由 EINTR 导致 bug 的例子。

在这种使用场景下,我们希望 Python 能够自行隐藏 InterruptedError 异常并自动重试系统调用。

Use Case 2: Be notified of signals as soon as possible

有时候,我们希望能够立即捕获并处理某些信号。举个例子,你可能希望当在键盘按下快捷键 CTRL+c 时就立即退出程序。

另外,对于某些信号我们应用程序既不感兴趣也不希望被其打断。这里有两种方式来处理我们感兴趣的信号:

  • 设置一个自定义的信号处理器,这个信号处理器收到信号后抛出特定的异常类型,比如说为 SIGINT 信号抛出 KeyboardInterrupt 类型的异常。

  • 结合 Python 的信号唤醒文件描述符机制使用 select() 一类的 I/O 复用函数:详见函数 signal.set_wakeup_fd()

我们期望在这种情况下, Python 的信号处理器能够及时地接收处理信号,当信号处理器抛出异常时系统调用要么失败要么重试。

建议

PEP建议在底层实现捕获 EINTR 错误码并重试,也就是说,该功能应该被包装在标准库中(而不是在上层库和应用程序中)。

特别需要指出的是,在 Python 中当一个系统调用被 EINTR 中断时会调用相应的信号处理器(内部调用 PyErr_CheckSignals()函数)。如果该信号处理器抛出一个异常,那么系统调用失败并向上层传递异常。

否则,Python将自动重试系统调用。如果该系统调用设置了超时时间,超时时间将重新计算。

需要修改的函数

下面列举了一些需要修改以符合上述 PEP 的标准库函数:

  • open(), os.open(), io.open()

  • functions of the faulthandler module

  • os functions:

    • os.fchdir()
    • os.fchmod()
    • os.fchown()
    • os.fdatasync()
    • os.fstat()
    • os.fstatvfs()
    • os.fsync()
    • os.ftruncate()
    • os.mkfifo()
    • os.mknod()
    • os.posix_fadvise()
    • os.posix_fallocate()
    • os.pread()
    • os.pwrite()
    • os.read()
    • os.readv()
    • os.sendfile()
    • os.wait3()
    • os.wait4()
    • os.wait()
    • os.waitid()
    • os.waitpid()
    • os.write()
    • os.writev()
    • special cases: os.close() and os.dup2() now ignore EINTR error,
      the syscall is not retried
  • select.select(), select.poll.poll(), select.epoll.poll(),
    select.kqueue.control(), select.devpoll.poll()

  • socket.socket() methods:

    • accept()
    • connect() (except for non-blocking sockets)
    • recv()
    • recvfrom()
    • recvmsg()
    • send()
    • sendall()
    • sendmsg()
    • sendto()
  • signal.sigtimedwait(), signal.sigwaitinfo()

  • time.sleep()

(注意: selector 模块目前虽然捕获到 InterruptedError 异常时会自动重试,但是它不会重新计算超时时间。)

os.close, close() 以及 os.dup2() 方法例外: 它们会忽略掉 EINTR 错误而不会重试。原因比较复杂涉及到 Linux 底层,发生 EINTR 错误返回后相关的文件描述符可能真的已经被关闭了(注:所以就不用重试关闭)。参考下面的文章:

对于非阻塞套接字(non-blocking sockets) socket.socket.connect() 方法被信号(失败并返回 EINTR 错误)中断后不会自动重试。因为连接操作已经异步在后台运行(注:后台已经异步向对方发出连接请求,只需等待对方应答,重试的话会导致重复连接),这时调用者需要自己负责等待套接字变得 “可读” (例如使用 select.select() ),然后调用 socket.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 检查连接是否成功(getsockopt() 返回 0 表示成功)。

InterruptedError 异常处理

由于系统调用被信号中断后将会自动重试,所以 InterruptedError 异常将不会被抛出,那么上面 Python3.4中的情况 章节所写的捕获 InterruptedError 的代码便可以移除,这样可简化标准库。

向后兼容

试想这样一类应用程序,当其系统调用被 InterruptedError 中断后会被挂起。PEP的作者没有考虑这类应用程序,因为这类程序引入了另外一些问题,比如竞态条件(系统调用执行前被信号中断将可能导致死锁)。此外,这样的代码也没有可移植性。

无论如何,这些应用程序都必须被修复以处理不同的信号,兼容不同的平台和 Python 版本。一个可行的方法是设置一个信号处理器抛出一个明确定义的异常或者使用信号唤醒文件描述符。

应用程序使用事件循环驱动时,推荐使用 signal.set_wakeup_fd() 来处理信号中断。 Python 底层的信号处理器将会把信号 number 写入唤醒文件描述符,事件循环被唤醒后便可以根据信号 number 处理信号。这样一来,事件循环便能不受约束地处理各种信号(比如,事件循环可以被任何线程唤醒而不仅仅局限于主线程)。

附录

唤醒文件描述符

从 Python 3.3 开始 signal.set_wakeup_fd() 会把信号 number 写入唤醒文件描述符,而之前是写入一个null字节(’\0’)。这样一来 python3.3 之后我们就可以通过唤醒文件描述符区分不同的信号。

Linux 下有一个叫 signalfd() 的系统调用函数可以提供关于信号的更多信息。比如,通过该函数可以获得发送该信号的 pid 和 uid 。但是很遗憾在 Python 中该函数不可用(详见 issue 12304

在 Unix 上, asyncio 模块使用唤醒文件描述符来唤醒其事件循环。

多线程化

C 语言中信号处理器可以被任何线程调用,但是在 Python 中只能被主线程调用。

Python 的 C API 提供了 PyErr_SetInterrupt() 函数来调用 SIGINT 的信号处理器就是为了中断Python的主线程。

Windows 中的信号

控制事件(Control events)

Windows 中使用 “control events”:

  • CTRL_BREAK_EVENT: Break (SIGBREAK)
  • CTRL_CLOSE_EVENT: Close event
  • CTRL_C_EVENT: CTRL+C (SIGINT)
  • CTRL_LOGOFF_EVENT: Logoff
  • CTRL_SHUTDOWN_EVENT: Shutdown

SetConsoleCtrlHandler() 函数
可以用来安装一个 控制事件 处理器。

通过 GenerateConsoleCtrlEvent() 函数可以向一个进程发送CTRL_C_EVENTCTRL_BREAK_EVENT 事件。该函数功能在 Python 中是通过 os.kill() 函数来提供。

信号

Windows 中支持的信号列表:

  • SIGABRT
  • SIGBREAK (CTRL_BREAK_EVENT): signal only available on Windows
  • SIGFPE
  • SIGILL
  • SIGINT (CTRL_C_EVENT)
  • SIGSEGV
  • SIGTERM

SIGINT

在 Windows 中对于 SIGINT 信号,Python 默认的信号处理器会设置一个 Windows 事件对象: sigint_event

time.sleep() 函数是用 WaitForSingleObjectEx() 来实现的, time.sleep() 函数通过设置一个等待 sigint_event 对象的超时时间来达到效果。所以(在Windows)中 Sleep 是可以被信号中断的。

_winapi.WaitForMultipleObjects() 也是自动添加 sigint_event 到监视处理列表,因此也可以被信号中断。

fgets() 失败时 PyOS_StdioReadline() 也是使用 sigint_event 来检查是否按下 CTRL+c 或者 CTRL+z 。

Misc

The main issue is: handle EINTR in the stdlib

Open issues:

Closed issues:

Open issues:

Closed issues:

Implementation

The implementation is tracked in issue 23285. It was committed on
February 07, 2015.

Copyright

This document has been placed in the public domain.

..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8